using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;

namespace Wintellect.Touch
{
    /// <summary>
    /// Helper base class to provide state machine-like behavior for determining which gesture is occurring
    /// </summary>
    internal abstract class GestureAction
    {
        #region Fields

        private enum TapStatus
        {
            Pending,
            HoldPending,
            HoldOcurred,
            DragOcurringFirstTime,
            DragOcurring,
            DragOcurred,
            PinchOcurringFirstTime,
            PinchOcurring,
            PinchOcurred,
            DoubleTapPending,
            DoubleTapOcurred,
            Tap,
        }

        private static readonly TimeSpan TapAndHoldTimeout = TimeSpan.FromMilliseconds(800);
        private static readonly TimeSpan DoubleTapTimeout = TimeSpan.FromMilliseconds(500);

        private TapStatus _actionTapStatus;
        private Point _actionStartingPosition;
        private Point _actionCurrentPosition;
        private Point _actionPreviousSamplePosition;

        private readonly UIElement _gestureElement;

        private readonly DispatcherTimer _actionTimer = new DispatcherTimer();

        #endregion

        #region Constructor(s) and Initialization

        /// <summary>
        /// Initializes a new instance of the <see cref="GestureAction"/> class.
        /// </summary>
        /// <param name="gestureElement">The UI element.</param>
        protected GestureAction(UIElement gestureElement)
        {
            if (gestureElement == null) throw new ArgumentNullException("gestureElement");
            _gestureElement = gestureElement;
            _actionTimer.Tick += OnActionTimerTick;
        } 

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets a value indicating whether this instance is valid.
        /// </summary>
        /// <value><c>true</c> if this instance is valid; otherwise, <c>false</c>.</value>
        public Boolean IsValid
        {
            get { return HasBeenInitialized; }
        }


        #endregion

        #region Manipulation Notifications

        /// <summary>
        /// Notify this state machine that manipulation has started.
        /// </summary>
        /// <param name="currentPosition">The current position.</param>
        public void HandleSingleManipulationStarted(Point currentPosition)
        {
            _actionTimer.Stop();

            switch (ActionTapStatus)
            {
                case TapStatus.Pending:
                    ProcessManipulationStartForPending(currentPosition);
                    _actionTimer.Interval = TapAndHoldTimeout;
                    _actionTimer.Start();
                    break;
                case TapStatus.DoubleTapPending:
                    ProcessManipulationStartForDoubleTapPending();
                    ActionCompleted(this, EventArgs.Empty);
                    break;
                default:
                    Debug.Assert(false, String.Format("Invalid state = entered MouseDown while state = {0}", ActionTapStatus));
                    ActionCompleted(this, EventArgs.Empty);
                    break;
            }
        }

        /// <summary>
        /// Notify this state machine that a manipulation delta has occurred.
        /// </summary>
        /// <param name="currentPosition">The current position.</param>
        /// <param name="offsetFromOrigin">The offset from origin.</param>
        public void HandleSingleManipulationDelta(Point currentPosition, Point offsetFromOrigin)
        {
            // Check to make sure the distance is greater than the desired threshold before assuming it is any kind of drag (noise reduction)
            if (!IsMoveAboveThreshold(offsetFromOrigin))
            {
                return;
            }

            _actionTimer.Stop();

            switch (ActionTapStatus)
            {
                case TapStatus.HoldPending:
                    ProcessManipulationDeltaForHoldPending(currentPosition);
                    break;
                case TapStatus.PinchOcurring:
                case TapStatus.PinchOcurringFirstTime:
                    ProcessManipulationEndForPinchOccurring(currentPosition);
                    break;
                case TapStatus.DragOcurringFirstTime:
                case TapStatus.DragOcurring:
                    ProcessManipulationDeltaForDragOcurring(currentPosition);
                    break;
                default:
                    _actionTimer.Stop();
                    ActionCompleted(this, EventArgs.Empty);
                    // This is legal - moving mouse destroys any other gesture processing.
                    //Debug.Assert(false, String.Format("Invalid state = entered MouseMove while state = {0}", _mouseActionTapStatus));
                    break;
            }
        }

        /// <summary>
        /// Notify this state machine that manipulation has completed.
        /// </summary>
        /// <param name="currentPosition">The current position.</param>
        public void HandleManipulationCompleted(Point currentPosition)
        {
            _actionTimer.Stop();

            switch (ActionTapStatus)
            {
                case TapStatus.DragOcurring:
                case TapStatus.DragOcurringFirstTime:
                    ProcessManipulationEndForDragOcurring(currentPosition);
                    _actionTimer.Stop();
                    ActionCompleted(this, EventArgs.Empty);
                    break;
                case TapStatus.PinchOcurring:
                case TapStatus.PinchOcurringFirstTime:
                    ProcessManipulationEndForPinchOccurring(currentPosition);
                    _actionTimer.Stop();
                    ActionCompleted(this, EventArgs.Empty);
                    break;
                case TapStatus.HoldPending:
                    ProcessManipulationEndForHoldPending();
                    _actionTimer.Interval = DoubleTapTimeout;
                    _actionTimer.Start();
                    break;
                default:
                    Debug.Assert(false, String.Format("Invalid state = entered MouseUp while state = {0}", ActionTapStatus));
                    _actionTimer.Stop();
                    ActionCompleted(this, EventArgs.Empty);
                    break;
            }
        }

        public void HandleMultipleManipulationDelta(Point currentPosition, Point scaleDelta)
        {
            switch (ActionTapStatus)
            {
                case TapStatus.Pending:
                    ProcessManipulationForPinchStart(currentPosition);
                    break;
                case TapStatus.PinchOcurring:
                case TapStatus.PinchOcurringFirstTime:
                    ProcessManipulationDeltaForPinchOccurring(currentPosition, scaleDelta);
                    break;
                default:
                    _actionTimer.Stop();
                    ActionCompleted(this, EventArgs.Empty);
                    ActionTapStatus = TapStatus.Pending;
                    HandleMultipleManipulationDelta(currentPosition, scaleDelta);
                    break;
            }
        }

        #endregion

        #region Events and Handlers
        
        public event EventHandler ActionCompleted = delegate { };

        private void OnActionTimerTick(Object sender, EventArgs e)
        {
            _actionTimer.Stop();

            Debug.WriteLine("MouseActionTimerTick - TapStatus is {0}", ActionTapStatus);

            if (ActionTapStatus == TapStatus.HoldPending)
            {
                // Got to a timer tick while waiting for a hold - it must be a hold.
                ProcessGestureTimerForHoldPending();
                ActionCompleted(this, EventArgs.Empty);
                return;
            }
            if (ActionTapStatus == TapStatus.DoubleTapPending)
            {
                // Got to a timer tick while waiting for a double tap - must be a single tap.
                ProcessGestureTimerForDoubleTapPending();
                ActionCompleted(this, EventArgs.Empty);
                return;
            }
        }

        #endregion

        #region Private Properties
        /// <summary>
        /// Gets the tap status for this action
        /// </summary>
        /// <value>The action tap status.</value>
        private TapStatus ActionTapStatus
        {
            get { return _actionTapStatus; }
            set
            {
                Debug.WriteLine("Shifting ActionTapStatus from {0} to {1}", _actionTapStatus, value);
                _actionTapStatus = value;
            }
        }

        /// <summary>
        /// Gets or sets the starting position for this action.
        /// </summary>
        /// <value>The action starting position.</value>
        private Point ActionStartingPosition
        {
            get { return _actionStartingPosition; }
            set
            {
                _actionStartingPosition = value;
                _actionPreviousSamplePosition = value;
                _actionCurrentPosition = value;
            }
        }

        /// <summary>
        /// Gets or sets the action gesture orientation for drag operations.
        /// </summary>
        /// <value>The action gesture orientation.</value>
        private Orientation? ActionGestureOrientation { get; set; }

        /// <summary>
        /// Gets or sets the current position for this action.
        /// </summary>
        /// <value>The action current position.</value>
        private Point ActionCurrentPosition
        {
            get { return _actionCurrentPosition; }
            set
            {
                // Save the previous sample value so delta can be computed
                _actionPreviousSamplePosition = _actionCurrentPosition;
                // Set the current value
                _actionCurrentPosition = value;
            }
        }

        /// <summary>
        /// Gets the action sample offset from the previous sample's position.
        /// </summary>
        /// <value>The action sample delta.</value>
        private Point ActionSampleDelta
        {
            get { return ActionCurrentPosition.Delta(_actionPreviousSamplePosition); }
        }

        private Point ActionScaleDelta { get; set; }

        #endregion

        #region Capture/Release

        /// <summary>
        /// Initializes the resources for this action source.
        /// </summary>
        /// <returns></returns>
        public Boolean Initialize()
        {
            return OnInitialize();
        }

        /// <summary>
        /// Releases any initialized resources for this action source.
        /// </summary>
        public void Release()
        {
            OnRelease();
        } 

        #endregion

        #region Overridable Methods

        /// <summary>
        /// Provides implementation-specific initialization
        /// </summary>
        /// <returns></returns>
        protected virtual Boolean OnInitialize()
        {
            return true;
        }

        /// <summary>
        /// Provides implementation-specific Release functionality
        /// </summary>
        protected virtual void OnRelease()
        {
        }

        /// <summary>
        /// Gets a value indicating whether this instance has been initialized.
        /// </summary>
        /// <value>
        /// 	<c>true</c> if this instance has been initialized; otherwise, <c>false</c>.
        /// </value>
        protected virtual Boolean HasBeenInitialized
        {
            get { return true; }
        }

        /// <summary>
        /// Gets the implementation-specific move-threshold.
        /// </summary>
        /// <value>The move threshold.</value>
        protected abstract Double MoveThreshold { get; } 

        #endregion

        #region State Transition Helpers

        /// <summary>
        /// Determines whether the given offset is above the threshold for this GestureAction type
        /// </summary>
        /// <param name="offsetFromOrigin">The offset from origin.</param>
        /// <returns>
        /// 	<c>true</c> if [is move above threshold] [the specified offset from origin]; otherwise, <c>false</c>.
        /// </returns>
        private Boolean IsMoveAboveThreshold(Point offsetFromOrigin)
        {
            var distance = offsetFromOrigin.Offset();
            return (distance > MoveThreshold);
        }

        /// <summary>
        /// State transfer to handle when a pending state receives a manipulation start.
        /// </summary>
        /// <param name="startingPosition">The starting position.</param>
        private void ProcessManipulationStartForPending(Point startingPosition)
        {
            ActionStartingPosition = startingPosition;
            ActionTapStatus = TapStatus.HoldPending;
        }

        /// <summary>
        /// State transfer to handle when a double-tap-pending state receives a manipulation start.
        /// </summary>
        private void ProcessManipulationStartForDoubleTapPending()
        {
            ActionTapStatus = TapStatus.DoubleTapOcurred;
            ProcessGestures(GestureType.DoubleTap);
        }

        /// <summary>
        /// State transfer to handle when a drag-occurring state receives a manipulation end
        /// </summary>
        /// <param name="currentPosition">The current position.</param>
        private void ProcessManipulationEndForDragOcurring(Point currentPosition)
        {
            ActionCurrentPosition = currentPosition;
            ActionTapStatus = TapStatus.DragOcurred;
            ProcessGestures(GestureType.DragComplete);
        }

        /// <summary>
        /// State transfer to handle when a hold-pending state receives a manipulation end
        /// </summary>
        private void ProcessManipulationEndForHoldPending()
        {
            ActionTapStatus = TapStatus.DoubleTapPending;
        }

        /// <summary>
        /// State transfer to handle when a hold-pending state receives a manipulation delta
        /// </summary>
        /// <param name="currentPosition">The current position.</param>
        private void ProcessManipulationDeltaForHoldPending(Point currentPosition)
        {
            ActionCurrentPosition = currentPosition;
            ActionTapStatus = TapStatus.DragOcurringFirstTime;
            ProcessGestures(GestureType.FreeDrag);
        }

        /// <summary>
        /// State transfer to handle when a drag-occurring state receives a manipulation delta
        /// </summary>
        /// <param name="currentPosition">The current position.</param>
        private void ProcessManipulationDeltaForDragOcurring(Point currentPosition)
        {
            ActionCurrentPosition = currentPosition;
            ActionTapStatus = TapStatus.DragOcurring;
            ProcessGestures(GestureType.FreeDrag);
        }

        /// <summary>
        /// State transfer to handle when a hold-pending state receives a timer event
        /// </summary>
        private void ProcessGestureTimerForHoldPending()
        {
            ActionTapStatus = TapStatus.HoldOcurred;
            ProcessGestures(GestureType.Hold);
        }

        /// <summary>
        /// State transfer to handle when a double-tap-pending state receives a timer event
        /// </summary>
        private void ProcessGestureTimerForDoubleTapPending()
        {
            ActionTapStatus = TapStatus.Tap;
            ProcessGestures(GestureType.Tap);
        }

        private void ProcessManipulationForPinchStart(Point currentPosition)
        {
            ActionCurrentPosition = currentPosition;
            ActionTapStatus = TapStatus.PinchOcurringFirstTime;
            ProcessGestures(GestureType.Pinch);
        }

        private void ProcessManipulationDeltaForPinchOccurring(Point currentPosition, Point scaleDelta)
        {
            ActionCurrentPosition = currentPosition;
            ActionScaleDelta = scaleDelta;
            ActionTapStatus = TapStatus.PinchOcurring;
            ProcessGestures(GestureType.Pinch);
        }

        private void ProcessManipulationEndForPinchOccurring(Point currentPosition)
        {
            ActionCurrentPosition = currentPosition;
            ActionTapStatus = TapStatus.PinchOcurred;
            ProcessGestures(GestureType.PinchComplete);
        }

        private void ProcessGestures(GestureType gestureType)
        {
            Debug.WriteLine("Processing gesture request for {0}", gestureType);
            if (!ActionGestureOrientation.HasValue && (ActionSampleDelta.X != 0 || ActionSampleDelta.Y != 0))
            {
                ActionGestureOrientation = Math.Abs(ActionSampleDelta.X) >= Math.Abs(ActionSampleDelta.Y)
                                                     ? Orientation.Horizontal
                                                     : Orientation.Vertical;
            }

            switch (gestureType)
            {
                case GestureType.Tap:
                    RaiseGestureEvent(_gestureElement, helper => helper.TapEventHandler, () => new GestureEventArgs(ActionStartingPosition, ActionStartingPosition));
                    break;

                case GestureType.DoubleTap:
                    RaiseGestureEvent(_gestureElement, helper => helper.DoubleTapEventHandler,
                                      () => new GestureEventArgs(ActionStartingPosition, ActionStartingPosition));
                    break;

                case GestureType.Hold:
                    RaiseGestureEvent(_gestureElement, helper => helper.HoldEventHandler, () => new GestureEventArgs(ActionStartingPosition, ActionStartingPosition));
                    break;

                case GestureType.FreeDrag:
                    if (ActionSampleDelta.X != 0 || ActionSampleDelta.Y != 0)
                    {
                        if (ActionTapStatus == TapStatus.DragOcurringFirstTime)
                        {
                            RaiseGestureEvent(
                                _gestureElement, 
                                helper => helper.DragStartedEventHandler,
                                () => new DragStartedGestureEventArgs(ActionStartingPosition, ActionGestureOrientation.Value));
                        }

                        RaiseGestureEvent(
                            _gestureElement, 
                            helper => helper.DragDeltaEventHandler,
                            () => new DragDeltaGestureEventArgs(ActionStartingPosition, ActionCurrentPosition, ActionSampleDelta, ActionGestureOrientation.Value));
                    }
                    break;

                case GestureType.DragComplete:
                    if (ActionSampleDelta.X != 0 || ActionSampleDelta.Y != 0)
                    {
                        // raise drag
                        RaiseGestureEvent(
                            _gestureElement, 
                            helper => helper.DragCompletedEventHandler,
                            () => new DragCompletedGestureEventArgs(ActionStartingPosition, ActionCurrentPosition, ActionSampleDelta, ActionGestureOrientation.Value, new Point()));
                    }
                    break;
                //case GestureType.Pinch:
                //    {
                //        if (ActionTapStatus == TapStatus.PinchOcurringFirstTime)
                //        {
                //            //RaiseGestureEvent(
                //            //    _gestureElement,
                //            //    helper => helper.PinchStartedEventHandler,
                //            //    () => new PinchStartedGestureEventArgs(ActionStartingPosition, ActionStartingPosition2, ActionStartingPosition, ActionStartingPosition2));
                //            RaiseGestureEvent(
                //                _gestureElement,
                //                helper => helper.SimplePinchStartedEventHandler,
                //                () => new SimplePinchStartedEventArgs(ActionStartingPosition, ActionCurrentPosition));
                //        }

                //        //RaiseGestureEvent(
                //        //    _gestureElement,
                //        //    helper => helper.PinchDeltaEventHandler,
                //        //    () => new PinchGestureEventArgs
                //        //        (ActionStartingPosition, ActionStartingPosition2, samplePosition, samplePosition2));
                //        RaiseGestureEvent(
                //            _gestureElement,
                //            helper => helper.SimplePinchDeltaEventHandler,
                //            () => new SimplePinchGestureEventArgs(ActionStartingPosition, ActionCurrentPosition, ActionScaleDelta));
                //    }
                //    break;

                //case GestureType.PinchComplete:
                //    {
                //        //RaiseGestureEvent(
                //        //    _gestureElement,
                //        //    helper => helper.PinchCompletedEventHandler,
                //        //    () => new PinchGestureEventArgs(ActionStartingPosition, ActionStartingPosition, _lastSamplePosition, _lastSamplePosition2));
                //        RaiseGestureEvent(
                //            _gestureElement,
                //            helper => helper.SimplePinchCompletedEventHandler,
                //            () => new SimplePinchCompletedEventArgs(ActionStartingPosition, ActionStartingPosition));
                //    }
                //    break;


                #region Unused/Unsupported Gestures
                //case GestureType.Flick:
                //    // Do not raise any additional drag events that may be queued.
                //    RaiseGestureEvent(helper => helper.Flick,
                //                      () => new FlickGestureEventArgs(gestureOrigin, sampleDelta));
                //    break;
                #endregion
            }
        }

        /// <summary>
        /// This method does all the necessary work to raise a gesture event.
        /// </summary>
        /// <typeparam name="T">This is the type of <see cref="EventArgs"/> that will be raised.</typeparam>
        /// <param name="gestureElement">The gesture elements.</param>
        /// <param name="eventGetter">Gets the specific event to raise.</param>
        /// <param name="argsGetter">Lazy creator function for the <see cref="EventArgs"/>.</param>
        private static void RaiseGestureEvent<T>(UIElement gestureElement, Func<GestureListener, EventHandler<T>> eventGetter, Func<T> argsGetter) where T : GestureEventArgs
        {
            var helper = GestureService.GetGestureListenerInternal(gestureElement, false);
            if (helper != null)
            {
                var eventToRaise = eventGetter(helper);
                if (eventToRaise != null)
                {
                    var args = argsGetter();
                    args.OriginalSource = gestureElement;
                    eventToRaise(gestureElement, args);
                }
            }
        }

        #endregion
    }

    /// <summary>
    /// Extension Methods for the <see cref="Point"/> class
    /// </summary>
    internal static class PointExtensions
    {
        /// <summary>
        /// Computes the offset vector between two points
        /// </summary>
        /// <param name="reference">The reference.</param>
        /// <param name="newLocation">The new location.</param>
        /// <returns></returns>
        public static Point Delta(this Point reference, Point newLocation)
        {
            return new Point(newLocation.X - reference.X, newLocation.Y - reference.Y);
        }

        /// <summary>
        /// Computes the Offset indicated by the X & Y values of the given <see cref="Point"/>
        /// </summary>
        /// <param name="offset">The offset.</param>
        /// <returns></returns>
        public static Double Offset(this Point offset)
        {
            var distance = Math.Sqrt(Math.Pow(offset.X, 2) + Math.Pow(offset.Y, 2));
            return distance;
        }
    }
}